/*
 *    This program is free software; you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation; either version 2 of the License, or
 *    (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with this program; if not, write to the Free Software
 *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/*
 *    ClassPanel.java
 *    Copyright (C) 2000 University of Waikato, Hamilton, New Zealand
 *
 */

package weka.gui.visualize;

import weka.core.FastVector;
import weka.core.Instances;
import weka.core.Utils;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.JColorChooser;
import javax.swing.JLabel;
import javax.swing.JPanel;

/**
 * This panel displays coloured labels for nominal attributes and a spectrum for
 * numeric attributes. It can also be told to colour on the basis of an array of
 * doubles (this can be useful for displaying coloured labels that correspond to
 * a clusterers predictions).
 * 
 * @author Mark Hall (mhall@cs.waikato.ac.nz)
 * @author Malcolm Ware (mfw4@cs.waikato.ac.nz)
 * @version $Revision: 7059 $
 */
public class ClassPanel extends JPanel {

	/** for serialization */
	private static final long serialVersionUID = -7969401840501661430L;

	/**
	 * True when the panel has been enabled (ie after setNumeric or setNominal
	 * has been called
	 */
	private boolean m_isEnabled = false;

	/** True if the colouring attribute is numeric */
	private boolean m_isNumeric = false;

	/** The height of the spectrum for numeric class */
	private final int m_spectrumHeight = 5;

	/** The maximum value for the colouring attribute */
	private double m_maxC;

	/** The minimum value for the colouring attribute */
	private double m_minC;

	/** The size of the ticks */
	private final int m_tickSize = 5;

	/** Font metrics */
	private FontMetrics m_labelMetrics = null;

	/** The font used in labeling */
	private Font m_labelFont = null;

	/** The amount of space to leave either side of the legend */
	private int m_HorizontalPad = 0;

	/** The precision with which to display real values */
	private int m_precisionC;

	/** Field width for numeric values */
	private int m_fieldWidthC;

	/** The old width. */
	private int m_oldWidth = -9000;

	/** Instances being plotted */
	private Instances m_Instances = null;

	/** Index of the colouring attribute */
	private int m_cIndex;

	/** the list of colours to use for colouring nominal attribute labels */
	private FastVector m_colorList;

	/**
	 * An optional list of Components that use the colour list maintained by
	 * this class. If the user changes a colour using the colour chooser, then
	 * these components need to be repainted in order to display the change
	 */
	private FastVector m_Repainters = new FastVector();

	/**
	 * An optional list of listeners who want to know when a colour changes.
	 * Listeners are notified via an ActionEvent
	 */
	private FastVector m_ColourChangeListeners = new FastVector();

	/** default colours for colouring discrete class */
	protected Color[] m_DefaultColors = { Color.blue, Color.red, Color.green,
			Color.cyan, Color.pink, new Color(255, 0, 255), Color.orange,
			new Color(255, 0, 0), new Color(0, 255, 0), Color.white };

	/**
	 * if set, it allows this panel to steer away from setting up a color in the
	 * color list that is equal to the background color
	 */
	protected Color m_backgroundColor = null;

	/**
	 * Inner Inner class used to create labels for nominal attributes so that
	 * there color can be changed.
	 */
	private class NomLabel extends JLabel {

		/** for serialization */
		private static final long serialVersionUID = -4686613106474820655L;

		private int m_index = 0;

		/**
		 * Creates a label with its name and class index value.
		 * 
		 * @param name
		 *            The name of the nominal class value.
		 * @param id
		 *            The index value for that class value.
		 */
		public NomLabel(String name, int id) {
			super(name);
			m_index = id;

			this.addMouseListener(new MouseAdapter() {
				public void mouseClicked(MouseEvent e) {

					if ((e.getModifiers() & e.BUTTON1_MASK) == e.BUTTON1_MASK) {
						Color tmp = JColorChooser.showDialog(ClassPanel.this,
								"Select new Color",
								(Color) m_colorList.elementAt(m_index));

						if (tmp != null) {
							m_colorList.setElementAt(tmp, m_index);
							m_oldWidth = -9000;
							ClassPanel.this.repaint();
							if (m_Repainters.size() > 0) {
								for (int i = 0; i < m_Repainters.size(); i++) {
									((Component) (m_Repainters.elementAt(i)))
											.repaint();
								}
							}

							if (m_ColourChangeListeners.size() > 0) {
								for (int i = 0; i < m_ColourChangeListeners
										.size(); i++) {
									((ActionListener) (m_ColourChangeListeners
											.elementAt(i)))
											.actionPerformed(new ActionEvent(
													this, 0, ""));
								}
							}
						}
					}
				}
			});
		}
	}

	public ClassPanel() {
		this(null);
	}

	public ClassPanel(Color background) {
		m_backgroundColor = background;

		/** Set up some default colours */
		m_colorList = new FastVector(10);
		for (int noa = m_colorList.size(); noa < 10; noa++) {
			Color pc = m_DefaultColors[noa % 10];
			int ija = noa / 10;
			ija *= 2;
			for (int j = 0; j < ija; j++) {
				pc = pc.darker();
			}

			m_colorList.addElement(pc);
		}
	}

	/**
	 * Adds a component that will need to be repainted if the user changes the
	 * colour of a label.
	 * 
	 * @param c
	 *            the component to be repainted
	 */
	public void addRepaintNotify(Component c) {
		m_Repainters.addElement(c);
	}

	/**
	 * Add an action listener that will be notified if the user changes the
	 * colour of a label
	 * 
	 * @param a
	 *            an <code>ActionListener</code> value
	 */
	public void addActionListener(ActionListener a) {
		m_ColourChangeListeners.addElement(a);
	}

	/**
	 * Set up fonts and font metrics
	 * 
	 * @param gx
	 *            the graphics context
	 */
	private void setFonts(Graphics gx) {
		if (m_labelMetrics == null) {
			m_labelFont = new Font("Monospaced", Font.PLAIN, 12);
			m_labelMetrics = gx.getFontMetrics(m_labelFont);
			int hf = m_labelMetrics.getAscent();
			if (this.getHeight() < (3 * hf)) {
				m_labelFont = new Font("Monospaced", Font.PLAIN, 11);
				m_labelMetrics = gx.getFontMetrics(m_labelFont);
			}
		}
		gx.setFont(m_labelFont);
	}

	/**
	 * Enables the panel
	 * 
	 * @param e
	 *            true to enable the panel
	 */
	public void setOn(boolean e) {
		m_isEnabled = e;
	}

	/**
	 * Set the instances.
	 * 
	 * @param insts
	 *            the instances
	 */
	public void setInstances(Instances insts) {
		m_Instances = insts;
	}

	/**
	 * Set the index of the attribute to display coloured labels for
	 * 
	 * @param cIndex
	 *            the index of the attribute to display coloured labels for
	 */
	public void setCindex(int cIndex) {
		if (m_Instances.numAttributes() > 0) {
			m_cIndex = cIndex;
			if (m_Instances.attribute(m_cIndex).isNumeric()) {
				setNumeric();
			} else {
				if (m_Instances.attribute(m_cIndex).numValues() > m_colorList
						.size()) {
					extendColourMap();
				}
				setNominal();
			}
		}
	}

	/**
	 * Extends the list of colours if a new attribute with more values than the
	 * previous one is chosen
	 */
	private void extendColourMap() {
		if (m_Instances.attribute(m_cIndex).isNominal()) {
			for (int i = m_colorList.size(); i < m_Instances
					.attribute(m_cIndex).numValues(); i++) {
				Color pc = m_DefaultColors[i % 10];
				int ija = i / 10;
				ija *= 2;
				for (int j = 0; j < ija; j++) {
					pc = pc.brighter();
				}
				if (m_backgroundColor != null) {
					pc = Plot2D.checkAgainstBackground(pc, m_backgroundColor);
				}

				m_colorList.addElement(pc);
			}
		}
	}

	protected void setDefaultColourList(Color[] list) {
		m_DefaultColors = list;
	}

	/**
	 * Set a list of colours to use for colouring labels
	 * 
	 * @param cols
	 *            a list containing java.awt.Colors
	 */
	public void setColours(FastVector cols) {
		m_colorList = cols;
	}

	/**
	 * Sets the legend to be for a nominal variable
	 */
	protected void setNominal() {
		m_isNumeric = false;
		m_HorizontalPad = 0;
		setOn(true);
		m_oldWidth = -9000;

		this.repaint();
	}

	/**
	 * Sets the legend to be for a numeric variable
	 */
	protected void setNumeric() {
		m_isNumeric = true;
		/*
		 * m_maxC = mxC; m_minC = mnC;
		 */

		double min = Double.POSITIVE_INFINITY;
		double max = Double.NEGATIVE_INFINITY;
		double value;

		for (int i = 0; i < m_Instances.numInstances(); i++) {
			if (!m_Instances.instance(i).isMissing(m_cIndex)) {
				value = m_Instances.instance(i).value(m_cIndex);
				if (value < min) {
					min = value;
				}
				if (value > max) {
					max = value;
				}
			}
		}

		// handle case where all values are missing
		if (min == Double.POSITIVE_INFINITY)
			min = max = 0.0;

		m_minC = min;
		m_maxC = max;

		int whole = (int) Math.abs(m_maxC);
		double decimal = Math.abs(m_maxC) - whole;
		int nondecimal;
		nondecimal = (whole > 0) ? (int) (Math.log(whole) / Math.log(10)) : 1;

		m_precisionC = (decimal > 0) ? (int) Math.abs(((Math.log(Math
				.abs(m_maxC)) / Math.log(10)))) + 2 : 1;
		if (m_precisionC > VisualizeUtils.MAX_PRECISION) {
			m_precisionC = 1;
		}

		String maxStringC = Utils.doubleToString(m_maxC, nondecimal + 1
				+ m_precisionC, m_precisionC);
		if (m_labelMetrics != null) {
			m_HorizontalPad = m_labelMetrics.stringWidth(maxStringC);
		}

		whole = (int) Math.abs(m_minC);
		decimal = Math.abs(m_minC) - whole;
		nondecimal = (whole > 0) ? (int) (Math.log(whole) / Math.log(10)) : 1;

		m_precisionC = (decimal > 0) ? (int) Math.abs(((Math.log(Math
				.abs(m_minC)) / Math.log(10)))) + 2 : 1;
		if (m_precisionC > VisualizeUtils.MAX_PRECISION) {
			m_precisionC = 1;
		}

		maxStringC = Utils.doubleToString(m_minC,
				nondecimal + 1 + m_precisionC, m_precisionC);
		if (m_labelMetrics != null) {
			if (m_labelMetrics.stringWidth(maxStringC) > m_HorizontalPad) {
				m_HorizontalPad = m_labelMetrics.stringWidth(maxStringC);
			}
		}

		setOn(true);
		this.repaint();
	}

	/**
	 * Renders the legend for a nominal colouring attribute
	 * 
	 * @param gx
	 *            the graphics context
	 */
	protected void paintNominal(Graphics gx) {
		setFonts(gx);

		int numClasses;

		numClasses = m_Instances.attribute(m_cIndex).numValues();

		int maxLabelLen = 0;
		int idx = 0;
		int legendHeight;
		int w = this.getWidth();
		int hf = m_labelMetrics.getAscent();

		for (int i = 0; i < numClasses; i++) {
			if (m_Instances.attribute(m_cIndex).value(i).length() > maxLabelLen) {
				maxLabelLen = m_Instances.attribute(m_cIndex).value(i).length();
				idx = i;
			}
		}

		maxLabelLen = m_labelMetrics.stringWidth(m_Instances
				.attribute(m_cIndex).value(idx));

		if (((w - (2 * m_HorizontalPad)) / (maxLabelLen + 5)) >= numClasses) {
			legendHeight = 1;
		} else {
			legendHeight = 2;
		}

		int x = m_HorizontalPad;
		int y = 1 + hf;

		// do the first row
		int ci, mp;
		Color pc;
		int numToDo = ((legendHeight == 1) ? numClasses : (numClasses / 2));
		for (int i = 0; i < numToDo; i++) {

			gx.setColor((Color) m_colorList.elementAt(i));
			// can we fit the full label or will each need to be trimmed?
			if ((numToDo * maxLabelLen) > (w - (m_HorizontalPad * 2))) {
				String val;
				val = m_Instances.attribute(m_cIndex).value(i);

				int sw = m_labelMetrics.stringWidth(val);
				int rm = 0;
				// truncate string if necessary
				if (sw > ((w - (m_HorizontalPad * 2)) / (numToDo))) {
					int incr = (sw / val.length());
					rm = (sw - ((w - (m_HorizontalPad * 2)) / numToDo)) / incr;
					if (rm <= 0) {
						rm = 0;
					}
					if (rm >= val.length()) {
						rm = val.length() - 1;
					}
					val = val.substring(0, val.length() - rm);
					sw = m_labelMetrics.stringWidth(val);
				}
				NomLabel jj = new NomLabel(val, i);
				jj.setFont(gx.getFont());

				jj.setSize(m_labelMetrics.stringWidth(jj.getText()),
						m_labelMetrics.getAscent() + 4);
				this.add(jj);
				jj.setLocation(x, y);
				jj.setForeground((Color) m_colorList.elementAt(i
						% m_colorList.size()));

				x += sw + 2;
			} else {

				NomLabel jj;
				jj = new NomLabel(m_Instances.attribute(m_cIndex).value(i), i);

				jj.setFont(gx.getFont());

				jj.setSize(m_labelMetrics.stringWidth(jj.getText()),
						m_labelMetrics.getAscent() + 4);
				this.add(jj);
				jj.setLocation(x, y);
				jj.setForeground((Color) m_colorList.elementAt(i
						% m_colorList.size()));

				x += ((w - (m_HorizontalPad * 2)) / numToDo);
			}
		}

		x = m_HorizontalPad;
		y = 1 + hf + 5 + hf;
		for (int i = numToDo; i < numClasses; i++) {

			gx.setColor((Color) m_colorList.elementAt(i));
			if (((numClasses - numToDo + 1) * maxLabelLen) > (w - (m_HorizontalPad * 2))) {
				String val;
				val = m_Instances.attribute(m_cIndex).value(i);

				int sw = m_labelMetrics.stringWidth(val);
				int rm = 0;
				// truncate string if necessary
				if (sw > ((w - (m_HorizontalPad * 2)) / (numClasses - numToDo + 1))) {
					int incr = (sw / val.length());
					rm = (sw - ((w - (m_HorizontalPad * 2)) / (numClasses - numToDo)))
							/ incr;
					if (rm <= 0) {
						rm = 0;
					}
					if (rm >= val.length()) {
						rm = val.length() - 1;
					}
					val = val.substring(0, val.length() - rm);
					sw = m_labelMetrics.stringWidth(val);
				}
				// this is the clipped string
				NomLabel jj = new NomLabel(val, i);
				jj.setFont(gx.getFont());

				jj.setSize(m_labelMetrics.stringWidth(jj.getText()),
						m_labelMetrics.getAscent() + 4);

				this.add(jj);
				jj.setLocation(x, y);
				jj.setForeground((Color) m_colorList.elementAt(i
						% m_colorList.size()));

				x += sw + 2;
			} else {
				// this is the full string
				NomLabel jj;
				jj = new NomLabel(m_Instances.attribute(m_cIndex).value(i), i);

				jj.setFont(gx.getFont());

				jj.setSize(m_labelMetrics.stringWidth(jj.getText()),
						m_labelMetrics.getAscent() + 4);
				this.add(jj);
				jj.setLocation(x, y);
				jj.setForeground((Color) m_colorList.elementAt(i
						% m_colorList.size()));

				x += ((w - (m_HorizontalPad * 2)) / (numClasses - numToDo));
			}
		}

	}

	/**
	 * Renders the legend for a numeric colouring attribute
	 * 
	 * @param gx
	 *            the graphics context
	 */
	protected void paintNumeric(Graphics gx) {

		setFonts(gx);
		if (m_HorizontalPad == 0) {
			setCindex(m_cIndex);
		}

		int w = this.getWidth();
		double rs = 15;
		double incr = 240.0 / (double) (w - (m_HorizontalPad * 2));
		int hf = m_labelMetrics.getAscent();

		for (int i = m_HorizontalPad; i < (w - m_HorizontalPad); i++) {
			Color c = new Color((int) rs, 150, (int) (255 - rs));
			gx.setColor(c);
			gx.drawLine(i, 0, i, 0 + m_spectrumHeight);
			rs += incr;
		}

		int whole = (int) Math.abs(m_maxC);
		double decimal = Math.abs(m_maxC) - whole;
		int nondecimal;
		nondecimal = (whole > 0) ? (int) (Math.log(whole) / Math.log(10)) : 1;

		m_precisionC = (decimal > 0) ? (int) Math.abs(((Math.log(Math
				.abs(m_maxC)) / Math.log(10)))) + 2 : 1;
		if (m_precisionC > VisualizeUtils.MAX_PRECISION) {
			m_precisionC = 1;
		}

		String maxStringC = Utils.doubleToString(m_maxC, nondecimal + 1
				+ m_precisionC, m_precisionC);

		int mswc = m_labelMetrics.stringWidth(maxStringC);
		int tmsc = mswc;
		if (w > (2 * tmsc)) {
			gx.setColor(Color.black);
			gx.drawLine(m_HorizontalPad, (m_spectrumHeight + 5), w
					- m_HorizontalPad, (m_spectrumHeight + 5));

			gx.drawLine(w - m_HorizontalPad, (m_spectrumHeight + 5), w
					- m_HorizontalPad, (m_spectrumHeight + 5 + m_tickSize));

			gx.drawString(maxStringC, (w - m_HorizontalPad) - (mswc / 2),
					(m_spectrumHeight + 5 + m_tickSize + hf));

			gx.drawLine(m_HorizontalPad, (m_spectrumHeight + 5),
					m_HorizontalPad, (m_spectrumHeight + 5 + m_tickSize));

			whole = (int) Math.abs(m_minC);
			decimal = Math.abs(m_minC) - whole;
			nondecimal = (whole > 0) ? (int) (Math.log(whole) / Math.log(10))
					: 1;

			m_precisionC = (decimal > 0) ? (int) Math.abs(((Math.log(Math
					.abs(m_minC)) / Math.log(10)))) + 2 : 1;

			if (m_precisionC > VisualizeUtils.MAX_PRECISION) {
				m_precisionC = 1;
			}

			maxStringC = Utils.doubleToString(m_minC, nondecimal + 1
					+ m_precisionC, m_precisionC);

			mswc = m_labelMetrics.stringWidth(maxStringC);
			gx.drawString(maxStringC, m_HorizontalPad - (mswc / 2),
					(m_spectrumHeight + 5 + m_tickSize + hf));

			// draw the middle value if there is space
			if (w > (3 * tmsc)) {
				double mid = m_minC + ((m_maxC - m_minC) / 2.0);
				gx.drawLine(
						m_HorizontalPad + ((w - (2 * m_HorizontalPad)) / 2),
						(m_spectrumHeight + 5), m_HorizontalPad
								+ ((w - (2 * m_HorizontalPad)) / 2),
						(m_spectrumHeight + 5 + m_tickSize));

				whole = (int) Math.abs(mid);
				decimal = Math.abs(mid) - whole;
				nondecimal = (whole > 0) ? (int) (Math.log(whole) / Math
						.log(10)) : 1;

				m_precisionC = (decimal > 0) ? (int) Math.abs(((Math.log(Math
						.abs(mid)) / Math.log(10)))) + 2 : 1;
				if (m_precisionC > VisualizeUtils.MAX_PRECISION) {
					m_precisionC = 1;
				}

				maxStringC = Utils.doubleToString(mid, nondecimal + 1
						+ m_precisionC, m_precisionC);

				mswc = m_labelMetrics.stringWidth(maxStringC);
				gx.drawString(maxStringC, m_HorizontalPad
						+ ((w - (2 * m_HorizontalPad)) / 2) - (mswc / 2),
						(m_spectrumHeight + 5 + m_tickSize + hf));
			}
		}
	}

	/**
	 * Renders this component
	 * 
	 * @param gx
	 *            the graphics context
	 */
	public void paintComponent(Graphics gx) {
		super.paintComponent(gx);
		if (m_isEnabled) {
			if (m_isNumeric) {
				m_oldWidth = -9000; // done so that if change back to nom, it
									// will
				// work
				this.removeAll();
				paintNumeric(gx);
			} else {
				if (m_Instances != null && m_Instances.numInstances() > 0
						&& m_Instances.numAttributes() > 0) {
					if (m_oldWidth != this.getWidth()) {
						this.removeAll();
						m_oldWidth = this.getWidth();
						paintNominal(gx);
					}
				}
			}
		}
	}

	/**
	 * Main method for testing this class.
	 * 
	 * @param args
	 *            first argument must specify an arff file. Second can specify
	 *            an optional index to colour labels on
	 */
	public static void main(String[] args) {
		try {
			if (args.length < 1) {
				System.err.println(Messages.getInstance().getString(
						"ClassPanel_Main_Error_Text_First"));
				System.exit(1);
			}
			final javax.swing.JFrame jf = new javax.swing.JFrame(Messages
					.getInstance().getString("ClassPanel_Main_JFrame_Text"));
			jf.setSize(500, 100);
			jf.getContentPane().setLayout(new BorderLayout());
			final ClassPanel p2 = new ClassPanel();
			jf.getContentPane().add(p2, BorderLayout.CENTER);
			jf.addWindowListener(new java.awt.event.WindowAdapter() {
				public void windowClosing(java.awt.event.WindowEvent e) {
					jf.dispose();
					System.exit(0);
				}
			});

			if (args.length >= 1) {
				System.err.println(Messages.getInstance().getString(
						"ClassPanel_Main_Error_Text_Second")
						+ args[0]);
				java.io.Reader r = new java.io.BufferedReader(
						new java.io.FileReader(args[0]));
				Instances i = new Instances(r);
				i.setClassIndex(i.numAttributes() - 1);
				p2.setInstances(i);
			}
			if (args.length > 1) {
				p2.setCindex((Integer.parseInt(args[1])) - 1);
			} else {
				p2.setCindex(0);
			}
			jf.setVisible(true);
		} catch (Exception ex) {
			ex.printStackTrace();
			System.err.println(ex.getMessage());
		}
	}
}
